<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="coinfaces">

        <title>为 Quasar 项目添加 TypeScript 支持</title>

    <link rel="stylesheet" href="../../theme/css/style.css">
    <link rel="stylesheet" href="../../theme/css/pygments.css">



</head>

<body>
    <div class="content">
<h1>为 Quasar 项目添加 TypeScript 支持</h1>    <p>
        under
            <a href="../../tags/quasar/">quasar</a>
            <a href="../../tags/vue/">vue</a>
            <a href="../../tags/typescript/">typescript</a>
    </p>
    <p>
        in <a href="../../categories/tech/">tech</a>
    </p>
    <p>Published: 2019-06-04</p>

        <p>Last edit: 2019-06-08</p>

    <h1>为 Quasar 项目添加 TypeScript 支持</h1>
<p>描述如何为一个 Vue 项目添加 TypeScript 支持的文章不少，<a href="https://johnpapa.net/vue-typescript/">Vue.js with TypeScript</a> 可以作为起点。</p>
<h2>解决软件依赖</h2>
<p>开发环境需要添加以下 npm 软件包：</p>
<div class="highlight"><pre><span></span><code><span class="err">npm install typescript ts-loader --save-dev</span>
</code></pre></div>


<h3>Vue 类组件</h3>
<p>下面两个依赖的作用是支持<a href="https://github.com/vuejs/vue-class-component">把 Vue 组件写成类的形式</a>，Microsoft 提供了一篇文档说明<a href="https://github.com/microsoft/TypeScript-Vue-Starter#using-decorators-to-define-a-component">如何使用类和属性装饰器编写 Vue 组件</a>：</p>
<div class="highlight"><pre><span></span><code><span class="err">npm install vue-class-component vue-property-decorator --save</span>
</code></pre></div>


<p>根据我有限的经验，类组件本身就是导出的组件，不需要借助 <code>Vue.extend</code> 进行转化，比普通 JavaScript 对象更加直观，去除了 <code>data</code>、<code>methods</code>、<code>computed</code> 这些包装后，这部分代码减少了一层缩进，更加简洁。</p>
<p>有2个不足：</p>
<ol>
<li>在 SFC 中使用这个组件时，Vetur 不再提供该组件的 <code>props</code> 自动完成。</li>
<li>
<p>如果该中的其余代码中引用了 <code>props</code> 定义的属性，TypeScipt 会因为该属性不存在无法通过编译。这时如果非要使用类组件，就得先用 <code>Vue.extend</code> 定义一个父类，再让组件继承这个父类，摘自 vue-class-component 项目<a href="https://github.com/vuejs/vue-class-component/blob/master/example/src/App.vue">提供的例子</a>：</p>
<p>```typescript
// We declare the props separately
// to make props types inferable.
const AppProps = Vue.extend({
  props: {
    propMessage: String
  }
})</p>
<p>@Component({ /<em> other stuff </em>/ })
export default class App extends AppProps {
  /<em> Implement class App </em>/
}
```</p>
</li>
</ol>
<p><a href="https://github.com/kaorun343/vue-property-decorator">vue-property-decorator</a> 比较好的解决了第2个问题，可以通过使用 <code>@Prop()</code> 装饰类属性的方法定义 <code>props</code>，除了少写了一部分代码，现在可以给这些属性添加类型定义以便 TypeScript 对引用这些属性的代码进行类型检查：</p>
<div class="highlight"><pre><span></span><code><span class="kr">import</span> <span class="p">{</span> <span class="nx">Vue</span><span class="p">,</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">Prop</span> <span class="p">}</span> <span class="nx">from</span> <span class="s1">&#39;vue-property-decorator&#39;</span>

<span class="kd">@Component</span>
<span class="kr">export</span> <span class="k">default</span> <span class="kr">class</span> <span class="nx">YourComponent</span> <span class="kr">extends</span> <span class="nx">Vue</span> <span class="p">{</span>
  <span class="kd">@Prop</span><span class="p">(</span><span class="nb">Number</span><span class="p">)</span> <span class="nx">readonly</span> <span class="nx">propA?</span>: <span class="kt">number</span>
  <span class="kd">@Prop</span><span class="p">({</span> <span class="k">default</span><span class="o">:</span> <span class="s1">&#39;default value&#39;</span> <span class="p">})</span> <span class="nx">readonly</span> <span class="nx">propB</span><span class="o">!:</span> <span class="kt">string</span>
<span class="p">}</span>
</code></pre></div>


<p>除此之外，对 <code>props</code> 定义的组件属性进行类型检查的方法我只想到一种，就是把每个引用组件属性的地方写成如下形式：</p>
<div class="highlight"><pre><span></span><code><span class="kr">const</span> <span class="nx">propValue</span>: <span class="kt">PropType</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">propName</span>
<span class="c1">// Use propValue</span>
</code></pre></div>


<p>想象一下这种方法可能带来的重复和工作量。</p>
<p>组件属性的值不是通过构造函数传递给组件的，这里引入了 <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html">Assignment Assertion Modifier 语法</a>，即通过在变量名后面添加感叹号的方式（<code>propB!: string</code>）告诉 TypeScript 这个属性既不在定义类的时候初始化，也不在构造函数中初始化，让 TypeScript 不再提示属性没有初始化的类型错误。Quasar 默认配置 eslint 的 parser babel-eslint，不能识别这种语法，解决方法是安装 typescript-eslint/parser，把 .eslintrc.js 中的 parser 设置为 @typescript-eslint/parser：</p>
<div class="highlight"><pre><span></span><code>   <span class="nx">parserOptions</span><span class="o">:</span> <span class="p">{</span>
     <span class="nx">parser</span><span class="o">:</span> <span class="s1">&#39;@typescript-eslint/parser&#39;</span><span class="p">,</span>
     <span class="nx">sourceType</span><span class="o">:</span> <span class="s1">&#39;module&#39;</span>
   <span class="p">},</span>
</code></pre></div>


<h2>在项目中添加 tsconfig.json 文件</h2>
<p>因为使用 VSCode 的缘故，直接把 jsconfig.json 重命名为 tsconfig.json，并添加如下设置：</p>
<div class="highlight"><pre><span></span><code><span class="p">{</span>
  <span class="nt">&quot;compilerOptions&quot;</span><span class="p">:</span> <span class="p">{</span>
    <span class="nt">&quot;allowJs&quot;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="nt">&quot;module&quot;</span><span class="p">:</span> <span class="s2">&quot;es2015&quot;</span><span class="p">,</span>
    <span class="nt">&quot;moduleResolution&quot;</span><span class="p">:</span> <span class="s2">&quot;node&quot;</span><span class="p">,</span>
    <span class="nt">&quot;outDir&quot;</span><span class="p">:</span> <span class="s2">&quot;./dist/typescript&quot;</span><span class="p">,</span>
    <span class="nt">&quot;target&quot;</span><span class="p">:</span> <span class="s2">&quot;es5&quot;</span><span class="p">,</span>
    <span class="nt">&quot;strict&quot;</span><span class="p">:</span> <span class="kc">true</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>


<p>该设置来自于 <a href="https://vuejs.org/v2/guide/typescript.html">Vue 文档</a>，注意这里并没有设置 <code>"lib"</code> 选项，如果要在 TypeScript 代码中用到 ES6 特性，应该需要添加该设置。</p>
<p>因为原来的项目使用 JavaScript，这里添加了 <code>"allowJs"</code> 选项，达到 JavaScript 和 TypeScript 共存的目的，但这个选项必须和 <code>"outDir"</code> 配合使用，因为 TypeScript 默认将目标代码输出为输入文件同目录下的 .js 文件，这将导致输入文件和输出文件路径冲突，实际上不会，因为项目中使用 Webpack ts-loader 来生成目标代码，但 VSCode 不了解这个信息，会提示如下错误：</p>
<blockquote>
<p>Cannot write file '/path/to/somefile.js' because it would overwrite input file.</p>
</blockquote>
<p>况且，如果不小心在项目目录中输入了 <code>tsc</code> 命令，TypeScript 真的会生成目标代码，在没有设置 <code>"outDir"</code> 的情况下，项目文件会被弄得乱七八糟。</p>
<h2>ES6 默认导出的问题</h2>
<p><a href="https://alexjover.com/blog/integrate-typescript-in-your-vue-project/">这篇文章</a>提到了 <code>"allowSyntheticDefaultImports"</code> 选项，这个选项的作用是使类似下面的语句通过类型检查：</p>
<div class="highlight"><pre><span></span><code><span class="kr">import</span> <span class="nx">Vue</span> <span class="nx">from</span> <span class="s1">&#39;vue&#39;</span>
</code></pre></div>


<p>它并不影响 TypeScript 输出的目标代码，如果 TypeScript 类型检查对此有疑问，并且也在项目中使用 TypeScript 生成目标代码，应该使用 <code>"esModuleInterop"</code> 选项，<a href="https://itnext.io/great-import-schism-typescript-confusion-around-imports-explained-d512fc6769c2">这篇文章</a>详细解释了原因。<a href="https://medium.com/@chimon1984/adding-typescript-support-to-quasar-eeec7e10a833">Adding TypeScript Support to Quasar</a> 没有使用这些选项，便增加了一个步骤：Fix Vue Imports &amp; Modules，把以上 <code>import</code> 语句改成：</p>
<div class="highlight"><pre><span></span><code><span class="kr">import</span> <span class="o">*</span> <span class="kr">as</span> <span class="nx">Vue</span> <span class="nx">from</span> <span class="s1">&#39;vue&#39;</span>
</code></pre></div>


<p>现在新项目应该不需要设置这些选项了，一方面 TypeScript 默认打开 <code>"esModuleInterop"</code>，参考 <code>tsc --init</code> 的输出，另外 Vue 也添加了默认导出：</p>
<div class="highlight"><pre><span></span><code><span class="kr">export</span> <span class="k">default</span> <span class="nx">Vue</span><span class="p">;</span>

<span class="kr">export</span> <span class="kr">as</span> <span class="nx">namespace</span> <span class="nx">Vue</span><span class="p">;</span>
</code></pre></div>


<p>可见这个问题让大家操碎了心。</p>
<h2>修改 Webpack 配置</h2>
<p>Quasar 项目的 Webpack 配置由 Quasar 自动生成，在项目的 quasar.conf.js 中添加如下代码：</p>
<div class="highlight"><pre><span></span><code><span class="nx">extendWebpack</span> <span class="p">(</span><span class="nx">cfg</span><span class="p">)</span> <span class="p">{</span>
  <span class="cm">/* other stuff */</span>
  <span class="nx">cfg</span><span class="p">.</span><span class="nx">resolve</span><span class="p">.</span><span class="nx">extensions</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s1">&#39;.ts&#39;</span><span class="p">)</span>
  <span class="nx">cfg</span><span class="p">.</span><span class="nx">resolve</span><span class="p">.</span><span class="nx">extensions</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s1">&#39;.tsx&#39;</span><span class="p">)</span>

  <span class="nx">cfg</span><span class="p">.</span><span class="nx">module</span><span class="p">.</span><span class="nx">rules</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
    <span class="nx">test</span><span class="o">:</span> <span class="sr">/\.tsx?$/</span><span class="p">,</span>
    <span class="nx">loader</span><span class="o">:</span> <span class="s1">&#39;ts-loader&#39;</span><span class="p">,</span>
    <span class="nx">exclude</span><span class="o">:</span> <span class="sr">/node_modules/</span><span class="p">,</span>
    <span class="nx">options</span><span class="o">:</span> <span class="p">{</span>
      <span class="nx">appendTsSuffixTo</span><span class="o">:</span> <span class="p">[</span><span class="sr">/\.vue$/</span><span class="p">]</span>
    <span class="p">}</span>
  <span class="p">})</span>
<span class="p">}</span>
</code></pre></div>


<h2>添加类型定义</h2>
<p>在 src/ 目录中添加 vue-shim.d.ts，内容如下：</p>
<div class="highlight"><pre><span></span><code><span class="kr">declare</span> <span class="nx">module</span> <span class="s2">&quot;*.vue&quot;</span> <span class="p">{</span>
  <span class="kr">import</span> <span class="nx">Vue</span> <span class="nx">from</span> <span class="s2">&quot;vue&quot;</span><span class="p">;</span>
  <span class="kr">export</span> <span class="k">default</span> <span class="nx">Vue</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>


<p>这个文件的目的是告诉 TypeScript *.vue 文件的导出类型。VSCode 也会依据这个文件对 Vue 组件进行类型推断。这个文件需要放在 src/ 目录下，放在项目根目录无效，可能是因为 tsconfig.json 中设置了：</p>
<div class="highlight"><pre><span></span><code><span class="p">{</span>
  <span class="nt">&quot;include&quot;</span><span class="p">:</span> <span class="p">[</span>
    <span class="s2">&quot;./src/**/*&quot;</span>
  <span class="p">]</span>
<span class="p">}</span>
</code></pre></div>


<p><a href="https://medium.com/@chimon1984/adding-typescript-support-to-quasar-eeec7e10a833">Adding TypeScript Support to Quasar</a> 指出，Quasar 项目还需要一些额外的类型定义：</p>
<div class="highlight"><pre><span></span><code><span class="kr">declare</span> <span class="nx">module</span> <span class="s2">&quot;quasar&quot;</span>
<span class="kr">declare</span> <span class="kr">const</span> <span class="nx">__THEME</span>
</code></pre></div>


<p><a href="https://github.com/quasarframework/app-extension-typescript">Quasar 官方插件</a> 增加了一个类型定义：</p>
<div class="highlight"><pre><span></span><code><span class="kr">declare</span> <span class="nx">namespace</span> <span class="nx">NodeJS</span> <span class="p">{</span>
  <span class="kr">interface</span> <span class="nx">ProcessEnv</span> <span class="p">{</span>
    <span class="nx">NODE_ENV</span>: <span class="kt">string</span>
    <span class="nx">VUE_ROUTER_MODE</span><span class="o">:</span> <span class="s1">&#39;hash&#39;</span> <span class="o">|</span> <span class="s1">&#39;history&#39;</span> <span class="o">|</span> <span class="s1">&#39;abstract&#39;</span> <span class="o">|</span> <span class="kc">undefined</span>
    <span class="nx">VUE_ROUTER_BASE</span>: <span class="kt">string</span> <span class="o">|</span> <span class="kc">undefined</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>


<h2>需要保留为 js 的源文件</h2>
<p>Quasar Cli 会在编译项目时会对项目进行检查，如果读取不到特定的文件会终止编译，如 src/router/index.js，因此建议保留 Quasar Cli 生成的 JavaScript 源文件。</p>
<h2>Quasar 官方支持</h2>
<p>v1 版本以的 Quasar <a href="https://forum.quasar-framework.org/topic/3251/quasar-typescript-preview-available">开始支持 TypeScript</a>，可以看到源代码里增加了 <a href="https://github.com/quasarframework/quasar/tree/dev/ui/types">utils 模块的类型定义</a>。</p>
    </div>
<nav class="nav sidebar">
    <div class="siteinfo">
        <div class="sitename"><a href="../..">coinfaces</a></div>
        <div class="tagline">Another Side of Life</div>
    </div>

        <a href="../../categories/">categories</a>
        <a href="../..">&larr; Home</a>
</nav><footer class="footer">
    <span>
        &copy; jpli &middot;
        under <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA 4.0</a>
    </span>
    |
    <span>
        Powered by:
        <a href="http://blog.getpelican.com/">Pelican</a>
    </span>
</footer></body>
</html>